/**
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
 */

import { ESCAPE, LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes';
import { Overlay, OverlayContainer } from '@angular/cdk/overlay';
import { Component, DebugElement, NgZone, provideZoneChangeDetection, ViewChild } from '@angular/core';
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
import { provideNoopAnimations } from '@angular/platform-browser/animations';

import { NzConfigService } from 'ng-zorro-antd/core/config';
import { dispatchFakeEvent, dispatchKeyboardEvent, MockNgZone } from 'ng-zorro-antd/core/testing';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { provideNzIconsTesting } from 'ng-zorro-antd/icon/testing';
import {
  getFitContentPosition,
  NzImage,
  NzImageDirective,
  NzImageGroupComponent,
  NzImageModule,
  NzImagePreviewRef,
  NzImageService
} from 'ng-zorro-antd/image';

const SRC = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png';
const QUICK_SRC =
  '';
const PLACEHOLDER =
  '';
const FALLBACK =
  '';

describe('image', () => {
  let fixture: ComponentFixture<TestImageBasicsComponent>;
  let context: TestImageBasicsComponent;
  let debugElement: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        // todo: use zoneless
        provideZoneChangeDetection(),
        provideNoopAnimations(),
        { provide: Overlay, useClass: Overlay },
        {
          provide: NgZone,
          useFactory: () => new MockNgZone()
        }
      ]
    });
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(TestImageBasicsComponent);
    fixture.detectChanges();
    context = fixture.componentInstance;
    debugElement = fixture.debugElement;
  });

  it('should only latest src work', fakeAsync(() => {
    context.src = 'error.png';
    context.placeholder = null;
    const image = debugElement.nativeElement.querySelector('img');
    fixture.detectChanges();
    const oldBackLoadImage = context.nzImage.backLoadImage;
    context.src = SRC;
    fixture.detectChanges();
    tick(1000);
    context.nzImage.backLoadImage.dispatchEvent(new Event('load'));
    fixture.detectChanges();
    expect(image.src).toBe(SRC);
    tick(1000);
    oldBackLoadImage.dispatchEvent(new ErrorEvent('error'));
    fixture.detectChanges();
    expect(image.src).toBe(SRC);
    expect(context.nzImage.status).toBe('normal');
  }));

  it('should keep placeholder when latest src is loading', fakeAsync(() => {
    context.src = SRC;
    context.placeholder = PLACEHOLDER;
    const image = debugElement.nativeElement.querySelector('img');
    fixture.detectChanges();
    const oldBackLoadImage = context.nzImage.backLoadImage;
    const SECOND_SRC = 'https://test.com/SECOND_SRC.png';
    context.src = SECOND_SRC;
    fixture.detectChanges();
    tick(1000);
    oldBackLoadImage.dispatchEvent(new Event('load'));
    fixture.detectChanges();
    expect(image.src).toBe(PLACEHOLDER);
    tick(1000);
    context.nzImage.backLoadImage.dispatchEvent(new Event('load'));
    fixture.detectChanges();
    expect(image.src).toBe(SECOND_SRC);
  }));
});

describe('image placeholder', () => {
  let fixture: ComponentFixture<TestImagePlaceholderComponent>;
  let context: TestImagePlaceholderComponent;
  let debugElement: DebugElement;

  beforeEach(() => {
    // todo: use zoneless
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection(), provideNoopAnimations(), { provide: Overlay, useClass: Overlay }]
    });
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(TestImagePlaceholderComponent);
    fixture.detectChanges();
    context = fixture.componentInstance;
    debugElement = fixture.debugElement;
  });

  xit('should placeholder src work', () => {
    const image = debugElement.nativeElement.querySelector('img');
    const spy = spyOnProperty(image, 'src', 'set').and.callThrough();
    context.src = SRC;
    context.placeholder = PLACEHOLDER;
    fixture.detectChanges();
    expect(spy).toHaveBeenCalled();
    expect(spy).toHaveBeenCalledWith(PLACEHOLDER);
  });

  it('should hide placeholder when image loaded', fakeAsync(() => {
    context.src = QUICK_SRC;
    context.placeholder = PLACEHOLDER;
    const imageComponent = context.nzImage;
    const imageElement = imageComponent.getElement().nativeElement;
    fixture.detectChanges();
    tick(300);
    fixture.detectChanges();
    imageComponent.backLoadImage.dispatchEvent(new Event('load'));
    fixture.detectChanges();
    expect(imageElement.getAttribute('src')).toBe(QUICK_SRC);
  }));
});

describe('image fallback', () => {
  let fixture: ComponentFixture<TestImageFallbackComponent>;
  let context: TestImageFallbackComponent;
  let debugElement: DebugElement;

  beforeEach(() => {
    // todo: use zoneless
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection(), provideNoopAnimations(), { provide: Overlay, useClass: Overlay }]
    });
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(TestImageFallbackComponent);
    fixture.detectChanges();
    context = fixture.componentInstance;
    debugElement = fixture.debugElement;
  });

  it('should fallback src work', fakeAsync(() => {
    context.src = 'error.png';
    context.fallback = FALLBACK;
    fixture.detectChanges();
    context.image.backLoadImage.dispatchEvent(new ErrorEvent('error'));
    tick();
    fixture.detectChanges();
    const image = debugElement.nativeElement.querySelector('img');
    expect(image.src).toBe(FALLBACK);
  }));
});

describe('image preview', () => {
  let fixture: ComponentFixture<TestImagePreviewGroupComponent>;
  let context: TestImagePreviewGroupComponent;
  let debugElement: DebugElement;
  let overlayContainer: OverlayContainer;
  let overlayContainerElement: HTMLElement;
  let previewElement: HTMLElement | null;
  let configService: NzConfigService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        // todo: use zoneless
        provideZoneChangeDetection(),
        provideNoopAnimations(),
        provideNzIconsTesting(),
        { provide: Overlay, useClass: Overlay }
      ]
    });
  });

  beforeEach(inject([OverlayContainer, NzConfigService], (oc: OverlayContainer, cs: NzConfigService) => {
    overlayContainer = oc;
    configService = cs;
    overlayContainerElement = oc.getContainerElement();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestImagePreviewGroupComponent);
    fixture.detectChanges();
    context = fixture.componentInstance;
    debugElement = fixture.debugElement;
  });

  afterEach(() => {
    overlayContainer.ngOnDestroy();
  });

  function getPreviewWrapElement(): HTMLElement {
    return overlayContainerElement.querySelector('.ant-image-preview-wrap')! as HTMLElement;
  }

  function getPreviewRootElement(): HTMLDivElement {
    return overlayContainerElement.querySelector('.ant-image-preview-root')! as HTMLDivElement;
  }

  function getPreviewImageElement(): HTMLImageElement {
    return previewElement!.querySelector('.ant-image-preview-img')! as HTMLImageElement;
  }

  function tickChanges(time: number = 300): void {
    fixture.detectChanges();
    tick(time);
    fixture.detectChanges();
  }

  describe('nzDisablePreview', () => {
    it('should nzDisablePreview work outside group', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      context.disablePreview = true;
      fixture.detectChanges();
      context.nzImage.getElement().nativeElement.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      expect(previewElement).not.toBeTruthy();
      context.disablePreview = false;
      fixture.detectChanges();
      context.nzImage.getElement().nativeElement.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      expect(previewElement).toBeTruthy();
    }));

    it('should nzDisablePreview work inside group', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      context.disablePreview = true;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelector('img');
      image.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      expect(previewElement).not.toBeTruthy();
      context.disablePreview = false;
      fixture.detectChanges();
      image.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      expect(previewElement).toBeTruthy();
    }));
  });

  describe('ImagePreview', () => {
    it('should rotate, zoom and close and flip work', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelector('img');
      image.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      const imageElement = getPreviewImageElement();
      const operations = overlayContainerElement.querySelectorAll('.ant-image-preview-operations-operation');
      const close = operations[0];
      const zoomIn = operations[1];
      const zoomOut = operations[2];
      const rotateRight = operations[3];
      const rotateLeft = operations[4];
      const flipHorizontally = operations[5];
      const flipVertically = operations[6];
      dispatchFakeEvent(rotateLeft, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(1, 1, 1) rotate(-90deg)');
      dispatchFakeEvent(rotateRight, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(1, 1, 1) rotate(0deg)');
      dispatchFakeEvent(zoomIn, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(1.5, 1.5, 1) rotate(0deg)');
      dispatchFakeEvent(zoomOut, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(1, 1, 1) rotate(0deg)');
      dispatchFakeEvent(flipHorizontally, 'click');
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(-1, 1, 1) rotate(0deg)');
      tickChanges();
      dispatchFakeEvent(flipVertically, 'click');
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(-1, -1, 1) rotate(0deg)');
      tickChanges();
      dispatchFakeEvent(close, 'click');
      tickChanges();
      previewElement = getPreviewRootElement();
      expect(previewElement).not.toBeTruthy();
      discardPeriodicTasks();
      flush();
    }));

    it('should zoom in/out based on the zoom step value', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      context.zoomStep = 2;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelectorAll('img');
      image[2].click();
      tickChanges();
      previewElement = getPreviewRootElement();
      const imageElement = getPreviewImageElement();
      const operations = overlayContainerElement.querySelectorAll('.ant-image-preview-operations-operation');
      const zoomIn = operations[1];
      const zoomOut = operations[2];
      dispatchFakeEvent(zoomIn, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(3, 3, 1) rotate(0deg)');
      dispatchFakeEvent(zoomOut, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(1, 1, 1) rotate(0deg)');
      discardPeriodicTasks();
      flush();
    }));

    it('should have a default value of 0.5 for zoomStep', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      context.zoomStep = null;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelector('img');
      image.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      const imageElement = getPreviewImageElement();
      const operations = overlayContainerElement.querySelectorAll('.ant-image-preview-operations-operation');
      const zoomIn = operations[1];
      dispatchFakeEvent(zoomIn, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(1.5, 1.5, 1) rotate(0deg)');
      discardPeriodicTasks();
      flush();
    }));

    it('should the groupZoomStep variable be set for each image"s zoomStep', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      context.groupZoomStep = 5;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelectorAll('img');
      image[0].click();
      tickChanges();
      previewElement = getPreviewRootElement();
      const imageElement = getPreviewImageElement();
      const operations = overlayContainerElement.querySelectorAll('.ant-image-preview-operations-operation');
      const zoomIn = operations[1];
      dispatchFakeEvent(zoomIn, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(6, 6, 1) rotate(0deg)');
      discardPeriodicTasks();
      flush();
    }));

    it('should the groupZoomStep variable be set for each image"s zoomStep Except those that already have a zoomStep value', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      context.groupZoomStep = 5;
      context.zoomStep = 3;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelectorAll('img');
      image[2].click();
      tickChanges();
      previewElement = getPreviewRootElement();
      const imageElement = getPreviewImageElement();
      const operations = overlayContainerElement.querySelectorAll('.ant-image-preview-operations-operation');
      const zoomIn = operations[1];
      dispatchFakeEvent(zoomIn, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(4, 4, 1) rotate(0deg)');
      discardPeriodicTasks();
      flush();
    }));

    it('should global config work', fakeAsync(() => {
      configService.set('image', { nzScaleStep: 10 });
      context.firstSrc = QUICK_SRC;
      fixture.detectChanges();
      tickChanges();
      const image = debugElement.nativeElement.querySelectorAll('img');
      image[3].click();
      tickChanges();
      previewElement = getPreviewRootElement();
      const imageElement = getPreviewImageElement();
      const operations = overlayContainerElement.querySelectorAll('.ant-image-preview-operations-operation');
      const zoomIn = operations[1];
      dispatchFakeEvent(zoomIn, 'click');
      tickChanges();
      expect(imageElement!.getAttribute('style')).toContain('transform: scale3d(11, 11, 1) rotate(0deg)');
      discardPeriodicTasks();
      flush();
    }));

    it('should detect mouse zoom direction correctly', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      tickChanges();
      previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
      expect(previewInstance.isDragging).toEqual(true);
      let isZoomingInside = previewInstance['isZoomedInWithMouseWheel'](10);
      expect(isZoomingInside).toBeFalsy();
      isZoomingInside = previewInstance['isZoomedInWithMouseWheel'](-10);
      expect(isZoomingInside).toBeTruthy();
    }));

    it('should call correct methods when zooming in or out', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      tickChanges();
      previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
      previewInstance['zoom'] = 5;
      spyOn(previewInstance, 'onZoomOut');
      spyOn<NzSafeAny>(previewInstance, 'reCenterImage');
      previewInstance['handleImageScaleWhileZoomingWithMouse'](10);
      expect(previewInstance.onZoomOut).toHaveBeenCalled();
      expect(previewInstance['reCenterImage']).not.toHaveBeenCalled();

      previewInstance['zoom'] = 0.5;
      spyOn(previewInstance, 'onZoomIn');
      spyOn<NzSafeAny>(previewInstance, 'reCenterImage');
      previewInstance['handleImageScaleWhileZoomingWithMouse'](-10);
      expect(previewInstance.onZoomOut).toHaveBeenCalled();
      expect(previewInstance['reCenterImage']).toHaveBeenCalled();
    }));

    it('should close image preview when escape is pressed', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      previewInstance.ngOnInit();
      tickChanges();
      spyOn(previewInstance, 'onClose');

      const event: KeyboardEvent = new KeyboardEvent('keydown', {
        keyCode: ESCAPE
      });
      document.dispatchEvent(event);
      tick();

      expect(previewInstance.onClose).toHaveBeenCalled();
    }));

    it('should container click work', fakeAsync(() => {
      context.firstSrc = QUICK_SRC;
      fixture.detectChanges();
      const image = debugElement.nativeElement.querySelector('img');
      image.click();
      tickChanges();
      const previewWrap = getPreviewWrapElement();
      previewWrap.click();
      tickChanges();
      previewElement = getPreviewRootElement();
      expect(previewElement).not.toBeTruthy();
      discardPeriodicTasks();
      flush();
    }));

    it('should preview group work', fakeAsync(() => {
      context.firstSrc = SRC;
      context.secondSrc = QUICK_SRC;
      fixture.detectChanges();
      const images = debugElement.nativeElement.querySelectorAll('img');
      images[0].click();
      fixture.detectChanges();
      tick(300);
      fixture.detectChanges();
      previewElement = getPreviewRootElement();
      const left = previewElement!.querySelector('.ant-image-preview-switch-left')!;
      const right = previewElement!.querySelector('.ant-image-preview-switch-right')!;
      expect(left).toBeTruthy();
      expect(right).toBeTruthy();
      expect(left.classList.contains('ant-image-preview-switch-left-disabled')).toBeTrue();
      dispatchFakeEvent(right, 'click');
      fixture.detectChanges();
      tick(300);
      fixture.detectChanges();
      let previewImage = getPreviewImageElement();
      expect(previewImage.getAttribute('src')).toContain(QUICK_SRC);
      expect(right.classList.contains('ant-image-preview-switch-right-disabled')).toBeTrue();
      dispatchFakeEvent(left, 'click');
      fixture.detectChanges();
      tick(300);
      fixture.detectChanges();
      previewImage = getPreviewImageElement();
      expect(previewImage.getAttribute('src')).toContain(SRC);
    }));
  });

  describe('Service', () => {
    it('should switchTo, next, prev work', fakeAsync(() => {
      const images = [
        {
          src: 'https://img.alicdn.com/tfs/TB1g.mWZAL0gK0jSZFtXXXQCXXa-200-200.svg',
          width: '200px',
          height: '200px',
          alt: 'ng-zorro'
        },
        {
          src: 'https://img.alicdn.com/tfs/TB1Z0PywTtYBeNjy1XdXXXXyVXa-186-200.svg',
          width: '200px',
          height: '200px',
          alt: 'angular'
        }
      ];
      context.images = images;
      context.createUsingService();
      context.previewRef?.switchTo(1);
      tickChanges();
      previewElement = getPreviewRootElement();
      let previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[1].src);
      context.previewRef?.next();
      fixture.detectChanges();
      previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[1].src);
      context.previewRef?.prev();
      fixture.detectChanges();
      previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[0].src);
      context.previewRef?.next();
      fixture.detectChanges();
      previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[1].src);

      dispatchKeyboardEvent(overlayContainerElement, 'keydown', RIGHT_ARROW);
      tickChanges();
      previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[1].src);
      dispatchKeyboardEvent(overlayContainerElement, 'keydown', LEFT_ARROW);
      tickChanges();
      previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[0].src);
      dispatchKeyboardEvent(overlayContainerElement, 'keydown', RIGHT_ARROW);
      tickChanges();
      previewImageElement = getPreviewImageElement();
      expect(previewImageElement.src).toContain(images[1].src);
    }));
  });

  describe('Drag', () => {
    it('should drag released work', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      tickChanges();
      previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
      expect(previewInstance.isDragging).toEqual(true);
      spyOn(previewInstance, 'onDragEnd').and.callFake(function () {
        return true;
      });
      expect(previewInstance.position).toEqual({ x: 0, y: 0 });
    }));

    it('should onDragEnd be called after drag is ended', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      tickChanges();
      previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
      spyOn(previewInstance, 'onDragEnd').and.callFake(function () {
        return true;
      });
      const e: NzSafeAny = {};
      previewInstance.onDragEnd(e);
      expect(previewInstance['onDragEnd']).toHaveBeenCalled();
    }));

    it('should zoom to center when zoom is <= 1', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      spyOn<NzSafeAny>(previewInstance, 'reCenterImage');
      tickChanges();
      context.zoomStep = 0.25;
      (previewInstance as NzSafeAny).zoom = 1.1;
      previewInstance.onZoomOut();
      tickChanges();
      expect(previewInstance['reCenterImage']).toHaveBeenCalled();
    }));

    it('should position calculate correct', () => {
      let params = {
        width: 200,
        height: 200,
        top: 0,
        left: 0,
        clientWidth: 1080,
        clientHeight: 768
      };
      let pos = getFitContentPosition(params);
      expect(pos.x).toBe(0);
      expect(pos.y).toBe(0);

      params = {
        width: 2000,
        height: 1000,
        top: 0,
        left: 0,
        clientWidth: 1080,
        clientHeight: 768
      };
      pos = getFitContentPosition(params);
      expect(pos.x).toBeNull();
      expect(pos.y).toBeNull();

      params = {
        width: 2000,
        height: 1000,
        top: 100,
        left: 100,
        clientWidth: 1080,
        clientHeight: 768
      };
      pos = getFitContentPosition(params);
      expect(pos.x).toBe(460);
      expect(pos.y).toBe(116);

      params = {
        width: 2000,
        height: 1000,
        top: -200,
        left: -200,
        clientWidth: 1080,
        clientHeight: 768
      };
      pos = getFitContentPosition(params);
      expect(pos.x).toBeNull();
      expect(pos.y).toBeNull();

      params = {
        width: 1000,
        height: 500,
        top: -200,
        left: -200,
        clientWidth: 1080,
        clientHeight: 768
      };
      pos = getFitContentPosition(params);
      expect(pos.x).toBe(0);
      expect(pos.y).toBe(0);

      params = {
        width: 1200,
        height: 600,
        top: -200,
        left: -200,
        clientWidth: 1080,
        clientHeight: 768
      };

      pos = getFitContentPosition(params);
      expect(pos.x).toBe(-60);
      expect(pos.y).toBe(-84);

      params = {
        width: 1000,
        height: 900,
        top: -200,
        left: -200,
        clientWidth: 1080,
        clientHeight: 768
      };

      pos = getFitContentPosition(params);
      expect(pos.x).toBe(-40);
      expect(pos.y).toBe(-66);
    });
  });

  describe('Zoom with mouse', () => {
    it('should call proper methods', fakeAsync(() => {
      context.images = [{ src: QUICK_SRC }];
      context.createUsingService();
      const previewInstance = context.previewRef!.previewInstance;
      tickChanges();
      const e = jasmine.createSpyObj('e', ['preventDefault', 'stopPropagation']);
      spyOn<NzSafeAny>(previewInstance, 'handlerImageTransformationWhileZoomingWithMouse');
      spyOn<NzSafeAny>(previewInstance, 'handleImageScaleWhileZoomingWithMouse');
      spyOn<NzSafeAny>(previewInstance, 'updatePreviewImageWrapperTransform');
      spyOn<NzSafeAny>(previewInstance, 'updatePreviewImageTransform');
      spyOn<NzSafeAny>(previewInstance, 'markForCheck');
      previewInstance.wheelZoomEventHandler(e);
      expect(e.preventDefault).toHaveBeenCalled();
      expect(e.stopPropagation).toHaveBeenCalled();
      expect(previewInstance['handlerImageTransformationWhileZoomingWithMouse']).toHaveBeenCalled();
      expect(previewInstance['handleImageScaleWhileZoomingWithMouse']).toHaveBeenCalled();
      expect(previewInstance['updatePreviewImageWrapperTransform']).toHaveBeenCalled();
      expect(previewInstance['updatePreviewImageTransform']).toHaveBeenCalled();
      expect(previewInstance['markForCheck']).toHaveBeenCalled();
    }));
  });
});

@Component({
  imports: [NzImageModule],
  template: `<img nz-image [nzSrc]="src" [nzPlaceholder]="placeholder" />`
})
export class TestImageBasicsComponent {
  @ViewChild(NzImageDirective) nzImage!: NzImageDirective;
  src = '';
  placeholder: string | null = '';
}

@Component({
  imports: [NzImageModule],
  template: `<img nz-image [nzSrc]="src" [nzPlaceholder]="placeholder" [nzDisablePreview]="disablePreview" />`
})
export class TestImagePlaceholderComponent {
  @ViewChild(NzImageDirective) nzImage!: NzImageDirective;
  src = '';
  placeholder = '';
  disablePreview = true;
}

@Component({
  imports: [NzImageModule],
  template: `<img nz-image [nzSrc]="src" [nzFallback]="fallback" />`
})
export class TestImageFallbackComponent {
  @ViewChild(NzImageDirective) image!: NzImageDirective;
  src = '';
  fallback = '';
}

@Component({
  imports: [NzImageModule],
  template: `
    <nz-image-group [nzScaleStep]="groupZoomStep">
      <img nz-image [nzSrc]="firstSrc" [nzDisablePreview]="disablePreview" />
      <img nz-image [nzSrc]="secondSrc" [nzDisablePreview]="disablePreview" [nzScaleStep]="zoomStep" />
    </nz-image-group>
    <img nz-image [nzSrc]="firstSrc" [nzDisablePreview]="disablePreview" [nzScaleStep]="zoomStep" />
    <img nz-image [nzSrc]="firstSrc" [nzDisablePreview]="disablePreview" />
  `
})
export class TestImagePreviewGroupComponent {
  disablePreview = false;
  firstSrc = '';
  secondSrc = '';
  previewRef: NzImagePreviewRef | null = null;
  images: NzImage[] = [];
  zoomStep: number | null = null;
  groupZoomStep: number | null = null;

  @ViewChild(NzImageGroupComponent) nzImageGroup!: NzImageGroupComponent;
  @ViewChild(NzImageDirective) nzImage!: NzImageDirective;

  constructor(private nzImageService: NzImageService) {}

  createUsingService(): void {
    this.previewRef = this.nzImageService.preview(this.images, { nzZoom: 1.5, nzRotate: 0 });
  }
}
